package cn.donting.jme.swing;

import cn.donting.jme.expansion.attribute.ValueChangeListener;
import com.formdev.flatlaf.extras.components.FlatTextField;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.*;
import java.util.List;

public abstract class JNumberTextField<T extends Number> extends FlatTextField implements MouseWheelListener, DocumentListener, FocusListener {
    private static final Set<Character> integerChars = new HashSet<>();
    private static final Set<Character> floatChars = new HashSet<>();
    private final DecimalFormat df = new DecimalFormat("#.############################");

    static {
        char[] charArray = "-1234567890".toCharArray();
        for (char c : charArray) {
            integerChars.add(c);
        }
        charArray = "1234567890.-".toCharArray();
        for (char c : charArray) {
            floatChars.add(c);
        }
    }

    private T minValue;
    private T maxValue;

    private T value;

    protected T stepSize;

    private final Set<Character> characters;

    public JNumberTextField(T value, T stepSize) {
        setValue(value);
        getDocument().addDocumentListener(this);
        addFocusListener(this);
        setHorizontalAlignment(RIGHT);

        characters = isFloatNumber(value) ? floatChars : integerChars;
        addMouseWheelListener(this);
        this.stepSize = stepSize;
        PlainDocument document = (PlainDocument) getDocument();
        document.setDocumentFilter(new NumberDocumentFilter());
    }

    private List<ValueChangeListener<T>> valueChangeListeners = new ArrayList<>();

    /**
     * 设置 鼠标滚轮步长
     *
     * @param stepSize
     */
    public void setStepSize(T stepSize) {
        this.stepSize = stepSize;
    }

    public void setValue(T value) {
        value = valueRange(value);
        this.value = value;
        setText(value);
    }

    public void addValueChangeListener(ValueChangeListener<T> l) {
        valueChangeListeners.add(l);
    }

    public T getValue() {
        return value;
    }

    /***
     * 限定 最大值和 最小值
     * @param value
     * @return
     */
    private T valueRange(T value) {
        if (minValue != null && compareTo(minValue, value) > 0) {
            value = minValue;
        }
        if (maxValue != null && compareTo(maxValue, value) < 0) {
            value = maxValue;
        }
        return value;
    }


    private void updateText() {
        String text = getText();
        try {
            T parse = parse(text);
            parse = valueRange(parse);
            if (!parse.equals(value)) {
                value = parse;
                for (ValueChangeListener<T> valueChangeListener : valueChangeListeners) {
                    valueChangeListener.change(parse);
                }
            }
        } catch (NumberFormatException e) {

        }
    }

    @Override
    public void insertUpdate(DocumentEvent e) {
        updateText();
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
        updateText();
    }

    @Override
    public void changedUpdate(DocumentEvent e) {

    }

    @Override
    public void focusGained(FocusEvent e) {

    }

    @Override
    public void focusLost(FocusEvent e) {
        try {
            T format = format(getText());
            format = valueRange(format);
            if (format.toString().equals(getText())) {
                return;
            }
            setText(format);
        } catch (Exception ex) {
            setText(value);
        }
    }

    public void setMinValue(T minValue) {
        this.minValue = minValue;
    }

    public void setMaxValue(T maxValue) {
        this.maxValue = maxValue;
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
        if (!isFocusOwner()) {
            //在没有焦点的时候使上层的 ScrollPane 滚动
            JScrollPane jScrollPane = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
            if (jScrollPane == null) {
                return;
            }
            for (MouseWheelListener mouseWheelListener : jScrollPane.getMouseWheelListeners()) {
                mouseWheelListener.mouseWheelMoved(e);
            }
            return;
        }
        int wheelRotation = e.getWheelRotation();
        T add;
        if (wheelRotation < 0) {
            add = add(value, stepSize);
        } else {
            add = subtraction(value, stepSize);
        }
        add = valueRange(add);

        setText(add);
    }

    private void setText(T v) {
        if (isFloatNumber(v)) {
            String format = df.format(v);
            setText(format);
            return;
        }
        setText(v.toString());
    }

    private boolean isFloatNumber(Number number) {
        return number instanceof Float || number instanceof Double || number instanceof BigDecimal;
    }

    protected abstract int compareTo(T n1, T n2);

    protected abstract T parse(String text) throws NumberFormatException;

    protected abstract T format(String text) throws ParseException;

    protected abstract T add(T value, T value2);

    protected abstract T subtraction(T value, T value2);


    protected class NumberDocumentFilter extends DocumentFilter {

        @Override
        public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
            if (input(text)) {
                super.insertString(fb, offset, text, attr);
            }
        }

        @Override
        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
            if (input(text)) {
                super.replace(fb, offset, length, text, attrs);
            }
        }

        private boolean input(String text) {
            char[] charArray = text.toCharArray();
            for (char c : charArray) {
                if (!characters.contains(c)) {
                    return false;
                }
            }
            return true;
        }
    }
}
